正如从前一章所知，我们需要一个Token类和一个Lexer类，所以需要TokenKind枚举来为每个Token类提供唯一的编号。拥有一个集所有功能于一体的头文件和实现文件并不能扩展，TokenKind可以普遍使用，并放置在Basic组件中。Token和Lexer类属于Lexer组件，但位于不同的头文件和实现文件中。

有三种不同类型的标记:关键字、标点符号和标记，它们表示许多值的集合。例如CONST关键字，;分隔符和ident标记，它们分别表示源代码中的标识符。每个标记都需要枚举的成员名。关键字和标点符号具有可以用于消息的自然显示名称。

与许多编程语言一样，关键字是标识符的子集。要将标记分类为关键字，我们需要一个关键字过滤器，检查找到的标识符是否确实是关键字。这与C或C++中的行为相同，其中关键字也是标识符的子集。编程语言不断发展，可能会引入新的关键字。例如，最初的K\&R C语言没有用enum关键字定义枚举，所以应该出现一个标志来指示关键字的语言级别。

我们收集了几条信息，都属于TokenKind枚举的一个成员:枚举成员的标签、标点符号的拼写和关键字的标志。对于诊断消息，我们将信息集中存储在一个名为include/tinylang/Basic/tokenkind.def的.def文件中，该文件如下所示。需要注意的一点是，其关键字的前缀是kw\_:

\begin{cpp}
#ifndef TOK
#define TOK(ID)
#endif
#ifndef PUNCTUATOR
#define PUNCTUATOR(ID, SP) TOK(ID)
#endif
#ifndef KEYWORD
#define KEYWORD(ID, FLAG) TOK(kw_ ## ID)
#endif

TOK(unknown)
TOK(eof)
TOK(identifier)
TOK(integer_literal)

PUNCTUATOR(plus, "+")
PUNCTUATOR(minus, "-")
// …

KEYWORD(BEGIN , KEYALL)
KEYWORD(CONST , KEYALL)
// …

#undef KEYWORD
#undef PUNCTUATOR
#undef TOK
\end{cpp}

有了这些集中的定义，就可以很容易地在include/tinylang/Basic/TokenKind.h文件中创建TokenKind枚举，枚举也会有自己的命名空间，例如:

\begin{cpp}
#ifndef TINYLANG_BASIC_TOKENKINDS_H
#define TINYLANG_BASIC_TOKENKINDS_H
namespace tinylang {
    namespace tok {
        enum TokenKind : unsigned short {
#define TOK(ID) ID,
#include "TokenKinds.def"
        NUM_TOKENS
    };
\end{cpp}

现在应该熟悉填充数组的模式了，TOK宏定义为只返回ID。另外，还将NUM\_TOKENS定义为枚举的最后一个成员，用于表示已定义的标记的数量:

\begin{cpp}
        const char *getTokenName(TokenKind Kind);
        const char *getPunctuatorSpelling(TokenKind Kind);
        const char *getKeywordSpelling(TokenKind Kind);
    }
}

#endif
\end{cpp}

实现文件lib/Basic/tokenkind.cpp也使用.def文件来检索名称:

\begin{cpp}
#include "tinylang/Basic/TokenKinds.h"
#include "llvm/Support/ErrorHandling.h"

using namespace tinylang;

static const char * const TokNames[] = {
    #define TOK(ID) #ID,
    #define KEYWORD(ID, FLAG) #ID,
    #include "tinylang/Basic/TokenKinds.def"
    nullptr
};
\end{cpp}

标记的文本名称源自其枚举标签ID。有两个特点:

\begin{itemize}
\item
首先，一些操作符共享相同的前缀，例如<和<=。若当前查看的字符是<，则必须先检查下一个字符，再决定找到哪个标记。请记住，输入需要以空字节结束，若当前字符有效，则可以使用下一个字符:

\begin{cpp}
    case '<':
        if (*(CurPtr + 1) == '=')
            formTokenWithChars(token, CurPtr + 2,
                               tok::lessequal);
        else
            formTokenWithChars(token, CurPtr + 1, tok::less);
        break;
\end{cpp}

\item
另一个是，现在有更多的关键字。我们该如何处理?一个简单而快速的解决方案是用关键字填充散列表，这些关键字都存储在tokenkind.def文件中，可以在Lexer类的实例化期间完成。使用这种方法，可以使用附加标志过滤关键字，还可以支持不同级别的语言。这里，还不需要这种灵活性。头文件中，关键字过滤器定义如下，使用llvm::StringMap实例作为哈希表:

\begin{cpp}
    class KeywordFilter {
        llvm::StringMap<tok::TokenKind> HashTable;
        void addKeyword(StringRef Keyword,
                        tok::TokenKind TokenCode);
    public:
        void addKeywords();
\end{cpp}

getKeyword()返回给定字符串的标记类型，若字符串不代表关键字则返回默认值:

\begin{cpp}
    tok::TokenKind getKeyword(
            StringRef Name,
            tok::TokenKind DefaultTokenCode = tok::unknown) {
        auto Result = HashTable.find(Name);
        if (Result != HashTable.end())
            return Result->second;
        return DefaultTokenCode;
    }
};
\end{cpp}

实现文件中，填充关键字表:

\begin{cpp}
void KeywordFilter::addKeyword(StringRef Keyword,
                               tok::TokenKind TokenCode) {
    HashTable.insert(std::make_pair(Keyword, TokenCode));
}

void KeywordFilter::addKeywords() {
#define KEYWORD(NAME, FLAGS) \
    addKeyword(StringRef(#NAME), tok::kw_##NAME);
#include "tinylang/Basic/TokenKinds.def"
}
\end{cpp}

\end{itemize}

使用刚学到的技术，编写一个高效的词法分析器类并不困难。由于编译速度很重要，许多编译器使用手写词法分析器，其中一个例子就是clang。
